﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using ScrumTable.Common;
using ScrumTable.Common.Collections;

using CacheWorkItemBucket = ScrumTable.DL.Data.Generic.Cache.CacheElementListEntryBucket<ScrumTable.DL.Data.Generic.Cache.CacheWorkItem>;

#endregion


namespace ScrumTable.DL.Data.Generic.Cache
{
    /// <summary>
    /// Represents a cache for optimized access work items by their unique id. This
    /// is important during the import of server side changes.
    /// </summary>
    internal class CacheWorkItemStoreIndex : IEnumerable<CacheWorkItem>
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly CacheWorkItemHierarchy _indexByHierarchy
            = new CacheWorkItemHierarchy();

        private readonly IDictionary<string, CacheWorkItemStoreIndexEntry> _indexByUniqueId
            = new Dictionary<string, CacheWorkItemStoreIndexEntry>();

        private readonly IDictionary<int, CacheWorkItemStoreIndexEntry> _indexById
            = new Dictionary<int, CacheWorkItemStoreIndexEntry>();

        private readonly CacheProject _ownerProject;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the bucket of a workitem by its id.
        /// </summary>
        /// <param name="id">Specifies the unique workitem to retrieve.</param>
        /// <returns>Returns the retrieved element or throw an exception, if the workitem could not be found.</returns>
        internal CacheWorkItemBucket this[int id]
        {
            get { return _indexById[id].Bucket; }
        }

        /// <summary>
        /// Gets the number of elements stored in the list.
        /// </summary>
        internal int Count
        {
            get { return _indexByHierarchy.Count; }
        }

        /// <summary>
        /// Gets the bucket of a workitem by its unique id.
        /// </summary>
        /// <param name="uniqueId">Specifies the unique workitem to retrieve.</param>
        /// <returns>Returns the retrieved element or throw an exception, if the workitem could not be found.</returns>
        internal CacheWorkItemBucket this[string uniqueId]
        {
            get { return _indexByUniqueId[uniqueId].Bucket; }
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection and returns the bucket with its unique id.
        /// </summary>
        internal IEnumerable<KeyValuePair<ValueChain<string>, CacheWorkItemBucket>> HierarchicalBuckets
        {
            get { return _indexByHierarchy; }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        internal CacheWorkItemStoreIndex(CacheProject ownerProject)
        {
            PreCondition.AssertNotNull(ownerProject, "ownerProject");

            _ownerProject = ownerProject;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<CacheWorkItem>)this).GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        IEnumerator<CacheWorkItem> IEnumerable<CacheWorkItem>.GetEnumerator()
        {
            return (from workItemBucket in _indexByUniqueId
                    select workItemBucket.Value.Bucket.Element).GetEnumerator();
        }

        /// <summary>
        /// Checks whether the index contains a workitem with the given id.
        /// </summary>
        /// <param name="id">Specifies the id of the element to check.</param>
        /// <returns>Returns true if the current index contains a workitem with the given id.</returns>
        internal bool Contains(int id)
        {
            return _indexById.ContainsKey(id);
        }

        /// <summary>
        /// Checks whether the index contains a workitem with the given unique id.
        /// </summary>
        /// <param name="uniqueId">Specifies the unique id of the element to check.</param>
        /// <returns>Returns true if the current index contains a workitem with the given unique id.</returns>
        internal bool Contains(string uniqueId)
        {
            return _indexByUniqueId.ContainsKey(uniqueId);
        }

        /// <summary>
        /// Returns true, if the current list contains a value with the given parentIdChain.
        /// </summary>
        /// <param name="parentIdChain">Id of the element to check.</param>
        internal bool Contains(ValueChain<string> parentIdChain)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            return _indexByHierarchy.Contains(parentIdChain);
        }

        /// <summary>
        /// Updates the parent hierarchy (especially the interation location) of the
        /// given workitem with the specified unique id.
        /// </summary>
        /// <param name="uniqueId">Specifies the unique id of the work item to invalidate.</param>
        /// <param name="beforeMove">Specifies the action which should be executed before the work item is moved.</param>
        /// <param name="afterMove">Specifies the action which should be executed after the work item is moved.</param>
        internal void UpdateHierarchyMove(string uniqueId, Action beforeMove, Action afterMove)
        {
            PreCondition.AssertNotNull(beforeMove, "beforeMove");
            PreCondition.AssertNotNull(afterMove, "afterMove");

            if (!Contains(uniqueId)) { return; }

            // retrieve current index entry, which should be adjusted
            CacheWorkItemStoreIndexEntry indexEntry = _indexByUniqueId[uniqueId];

            // retrieve the index entry chain for the current element
            ValueChain<string> indexEntryChain = new ValueChain<string>(indexEntry.ParentChain, indexEntry.Bucket.Element.UniqueId);

            // evaluate old iteration chain from the current iteration chain.
            ValueChain<string> oldIterationChain = GetTopIterationChain(indexEntry.ParentChain);

            // evaluate new iteration unique id chain from new iteration path. the
            // parent link to another workitem is stronger than the iteration path. if
            // there's a parent workitem available, move the workitem into the parent
            // location cache.
            ValueChain<string> newIterationChain = GetIterationChain(indexEntry);

            // retrieve children
            var children = _indexByHierarchy.GetChildren(indexEntryChain).ToList();

            if (oldIterationChain != newIterationChain)
            {
                beforeMove();

                // move current bucket in the work item tree
                Move(indexEntry.Bucket,
                    indexEntry.ParentChain,
                    indexEntryChain.ReplaceParent(oldIterationChain, newIterationChain).ParentChain);

                // set iteration path on children
                children.ForEach(
                    childElement =>
                    {
                        // this will produce a recursive call of the UpdateHierarchyMove() function
                        // which is required in order to inform all children about the new iteration path
                        childElement.Value.Element.InvalidateIterationPath();
                    });

                afterMove();
            }
        }

        internal void UpdateHierarchyRemove(string uniqueId)
        {
            if (!Contains(uniqueId)) { return; }

            CacheWorkItemStoreIndexEntry indexEntry = _indexByUniqueId[uniqueId];

            // the element will be linked to its root (project -> iteration)
            Move(
                indexEntry,
                indexEntry.ParentChain,
                GetRootChain(indexEntry.Bucket.Element.Iteration));
        }

        internal void UpdateHierarchyAdd(string uniqueId)
        {
            if (!Contains(uniqueId)) { return; }

            CacheWorkItemStoreIndexEntry indexEntry = _indexByUniqueId[uniqueId];

            // update parent chain of the current item, move from root to new parent work item
            Move(
                indexEntry,
                indexEntry.ParentChain,
                GetParentChain(indexEntry.Bucket.Element));
        }

        /// <summary>
        /// Returns the work item with the given id.
        /// </summary>
        /// <param name="workItemId">Specifies the id of the work item to check.</param>
        internal CacheWorkItem TryGetById(int workItemId)
        {
            return _indexById.ContainsKey(workItemId) ? _indexById[workItemId].Bucket.Element : null;
        }

        /// <summary>
        /// Returns the work item with the given id.
        /// </summary>
        /// <param name="parentIdChain">Specifies the parent chain of the work item to retrieve.</param>
        /// <param name="childId">Specifies the unique id of the work item to retrieve.</param>
        internal CacheWorkItem TryGetByChain(ValueChain<string> parentIdChain, string childId)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");
            PreCondition.AssertNotNull(childId, "childId");

            if (_indexByHierarchy.Contains(parentIdChain, childId))
            {
                return _indexByHierarchy[parentIdChain, childId].Element;
            }
            return null;
        }

        /// <summary>
        /// Returns the workitems which corresponds to the given condition.
        /// </summary>
        /// <param name="parentIdChain">Specifies the parent chain of the workitems to retrieve.</param>
        /// <param name="toEvaluate">Specifies a condition which is executed for every workitem.</param>
        internal IEnumerable<CacheWorkItem> TryGetByChain(ValueChain<string> parentIdChain, Func<CacheWorkItem, bool> toEvaluate = null)
        {
            PreCondition.AssertNotNull(parentIdChain, "parentIdChain");

            return from wiBucket in _indexByHierarchy.GetChildren(parentIdChain)
                   where (toEvaluate == null || toEvaluate(wiBucket.Value.Element))
                   select wiBucket.Value.Element;
        }

        /// <summary>
        /// Returns the id chain of the work item associated with the given work item id.
        /// </summary>
        /// <param name="workItemId">Specifies the id of the work item to check.</param>
        internal ValueChain<string> TryGetChainById(int workItemId)
        {
            if (_indexById.ContainsKey(workItemId))
            {
                CacheWorkItemStoreIndexEntry entry = _indexById[workItemId];
                return new ValueChain<string>(entry.ParentChain, entry.Bucket.Element.UniqueId);
            }
            return ValueChain<string>.Empty;
        }

        /// <summary>
        /// Returns the id chain of the work item associated with the given work item id.
        /// </summary>
        /// <param name="workItemUnqiueId">Specifies the unique id of the work item to check.</param>
        internal ValueChain<string> TryGetChainById(string workItemUnqiueId)
        {
            if (_indexByUniqueId.ContainsKey(workItemUnqiueId))
            {
                return new ValueChain<string>(
                    _indexByUniqueId[workItemUnqiueId].ParentChain,
                    workItemUnqiueId);
            }
            return ValueChain<string>.Empty;
        }

        /// <summary>
        /// Registers the given bucket in the cache. This procedure WILL NOT register
        /// the bucket in the workitem cache tree. For this purpose use the RegisterInCacheTree() procedure
        /// instead. This procedure is usefull if you'd like to register a bucket in
        /// the cache index and add it later to the workitem tree after all workitems have
        /// been registred in the index for raster search.
        /// In this case the bucket is only a place holder for the new element to register.
        /// It does not provide any other information about the element or its revision.
        /// </summary>
        /// <param name="toRegister">Specifies the bucket to register.</param>
        internal void RegisterInCacheIndex(CacheWorkItemBucket toRegister)
        {
            PreCondition.AssertNotNull(toRegister, "toRegister");

            CacheWorkItemStoreIndexEntry indexEntry = new CacheWorkItemStoreIndexEntry
            {
                Bucket = toRegister,
                ParentChain = GetRootChain(toRegister.Element.Iteration)
            };

            _indexByUniqueId[toRegister.Element.UniqueId] = indexEntry;
            _indexById[toRegister.Element.Id] = indexEntry;
        }

        /// <summary>
        /// Adds the given bucket to the cache index.
        /// </summary>
        /// <returns>Returns the evaluated parent chain.</returns>
        internal ValueChain<string> RegisterInCacheTree(Func<int, bool> containerContains, Func<int, CacheWorkItem> containerGet, CacheWorkItemBucket toAdd)
        {
            PreCondition.AssertNotNull(containerContains, "containerContains");
            PreCondition.AssertNotNull(containerGet, "containerGet");
            PreCondition.AssertNotNull(toAdd, "toAdd");

            ValueChain<string> parentChain = GetParentChain(containerContains, containerGet, toAdd.Element);
            RegisterInCacheTree(parentChain, toAdd);
            return parentChain;
        }

        /// <summary>
        /// Adds the given bucket to the cache index.
        /// </summary>
        /// <param name="toAdd">Specifies the element to add.</param>
        /// <returns>Returns the evaluated parent chain.</returns>
        internal ValueChain<string> RegisterInCacheTree(CacheWorkItemBucket toAdd)
        {
            PreCondition.AssertNotNull(toAdd, "toAdd");

            ValueChain<string> parentChain = GetParentChain(toAdd.Element);
            RegisterInCacheTree(parentChain, toAdd);
            return parentChain;
        }

        /// <summary>
        /// Adds the given bucket to the cache tree.
        /// </summary>
        /// <param name="parentChain">Specifies the parent key chain of the element to add.</param>
        /// <param name="toAdd">Specifies the element to add.</param>
        internal void RegisterInCacheTree(ValueChain<string> parentChain, CacheWorkItemBucket toAdd)
        {
            if (Contains(toAdd.Element.UniqueId))
            {
                CacheWorkItemStoreIndexEntry indexEntry = _indexByUniqueId[toAdd.Element.UniqueId];
                ValueChain<string> oldParentChain = indexEntry.ParentChain;

                indexEntry.Bucket = toAdd;
                indexEntry.ParentChain = parentChain;

                // move the element, if it already exists in the cache
                Move(indexEntry, oldParentChain, parentChain);
            }
            else
            {
                CacheWorkItemStoreIndexEntry indexEntry = new CacheWorkItemStoreIndexEntry
                {
                    Bucket = toAdd,
                    ParentChain = parentChain
                };

                _indexByHierarchy[parentChain, indexEntry.Bucket.Element.UniqueId] = indexEntry.Bucket;
                _indexByUniqueId[toAdd.Element.UniqueId] = indexEntry;
                _indexById[toAdd.Element.Id] = indexEntry;
            }
        }

        internal void UpdateId(int oldId, int newId, string uniqueIdToUpdate)
        {
            CacheWorkItemStoreIndexEntry element = _indexByUniqueId[uniqueIdToUpdate];
            _indexById.Remove(oldId);
            _indexById[newId] = element;
        }


        private void Move(CacheWorkItemStoreIndexEntry toMove, ValueChain<string> oldParentChain, ValueChain<string> newParentChain)
        {
            // precondition: if there's no change, return
            if (oldParentChain == newParentChain)
            {
                // make sure the given index entry is registered in the cache tree.
                // this MUST be ensured in order the cache works properly
                _indexByHierarchy[newParentChain, toMove.Bucket.Element.UniqueId] = toMove.Bucket;
            }
            else
            {
                //  1. move current item
                Move(toMove.Bucket, oldParentChain, newParentChain);

                //  2. get child chains
                ValueChain<string> oldChain = new ValueChain<string>(oldParentChain, toMove.Bucket.Element.UniqueId);
                ValueChain<string> newChain = new ValueChain<string>(newParentChain, toMove.Bucket.Element.UniqueId);

                var itemsToMove = _indexByHierarchy.GetChildChains(oldChain);

                // 3. change chains of children below given element
                itemsToMove.ForEach(
                    itemToMoveChain =>
                    {
                        Move(
                            // retrieve object to move
                            _indexByHierarchy[itemToMoveChain.ParentChain, itemToMoveChain.Top],

                            itemToMoveChain.ParentChain,

                            // replace chains from old to new
                            itemToMoveChain.ReplaceParent(oldChain, newChain).ParentChain);
                    });
            }
        }

        private void Move(CacheWorkItemBucket toMove, ValueChain<string> oldParentChain, ValueChain<string> newParentChain)
        {
            // remove from old postition
            _indexByHierarchy.Remove(oldParentChain, toMove.Element.UniqueId);

            // modify parent link entry in id cache
            _indexByUniqueId[toMove.Element.UniqueId].ParentChain = newParentChain;

            // add element to new position
            _indexByHierarchy[newParentChain, toMove.Element.UniqueId] = toMove;
        }

        private ValueChain<string> GetIterationChain(CacheWorkItemStoreIndexEntry indexEntry)
        {
            if (_indexByUniqueId.ContainsKey(indexEntry.ParentChain.Top))
            {
                // if there's a parent work item get parent chain of it
                // out of the cache. the cache always contains the updated chains
                return GetTopIterationChain(_indexByUniqueId[indexEntry.ParentChain.Top].ParentChain);
            }
            
            // there's no parent work item available, directly retrieve the info from the workitem
            return GetRootChain(indexEntry.Bucket.Element.Iteration);
        }

        private ValueChain<string> GetTopIterationChain(ValueChain<string> parentChain)
        {
            return (_indexByUniqueId.ContainsKey(parentChain.Top))
                       ? GetTopIterationChain(parentChain.ParentChain)
                       : parentChain;
        }

        private ValueChain<string> GetRootChain(CacheIteration iteration)
        {
            ValueChainBuilder<string> chainBuilder = new ValueChainBuilder<string>();
            AddIterationAndProjectChain(chainBuilder, iteration);
            return chainBuilder.ToChain();
        }

        private ValueChain<string> GetParentChain(CacheWorkItem toImport)
        {
            return GetParentChain(
                index => _indexById.ContainsKey(index),
                index => _indexById[index].Bucket.Element,
                toImport);
        }

        private ValueChain<string> GetParentChain(Func<int, bool> containerContains, Func<int, CacheWorkItem> containerGet, CacheWorkItem toImport)
        {
            ValueChainBuilder<string> parentChain = new ValueChainBuilder<string>();
            IList<CacheWorkItem> parentTree = new List<CacheWorkItem>();

            AddParentChain(
                parentTree,
                parentChain,
                containerContains,
                containerGet,
                toImport);
            AddIterationAndProjectChain(
                parentChain,
                (parentTree.Count > 0) ? parentTree[parentTree.Count - 1].Iteration : toImport.Iteration);

            return parentChain.ToChain();
        }

        private static void AddParentChain(IList<CacheWorkItem> parentTree, ValueChainBuilder<string> chainBuilder, Func<int, bool> containerContains, Func<int, CacheWorkItem> containerGet, CacheWorkItem toImport)
        {
            CacheWorkItemLink parentLink = toImport.ParentLink;
            
            if (parentLink != null
                && containerContains(parentLink.RelatedId)
                && !chainBuilder.Contains(parentLink.RelatedUnqiueId))
            {
                CacheWorkItem parentItem = containerGet(parentLink.RelatedId);

                parentTree.Add(parentItem);
                chainBuilder.AddBottom(parentLink.RelatedUnqiueId);

                AddParentChain(parentTree, chainBuilder, containerContains, containerGet, parentItem);
            }
        }

        private void AddIterationAndProjectChain(ValueChainBuilder<string> chainBuilder, CacheIteration iteration)
        {
            if (iteration != null)
            {
                chainBuilder.AddBottom(iteration.UniqueId);
            }
            chainBuilder.AddBottom(_ownerProject.UniqueId);
        }

        #endregion

        private class CacheWorkItemStoreIndexEntry
        {
            internal ValueChain<string> ParentChain { get; set; }
            internal CacheWorkItemBucket Bucket { get; set; }
        }
    }
}
